上一篇在介紹 NestJS 的微服務應用程式以及相關概念,那要如何用 NestJS 與微服務應用程式溝通呢?NestJS 有為此設計了 客戶端(Client) 的相關機制。客戶端的定義與微服務應用程式差不多,指的是 使用與 HTTP 協定不同傳輸層進行溝通的客戶端。
NestJS 在客戶端也下了不少功夫,使用 Proxy Pattern 的方式設計了 ClientProxy
這個類別,背後會根據指定的 Transporter 來發送訊息,如此一來,無論是哪一種 Transporter 都可以用相同的方式來處理。
透過 NestCLI 產生一個專案:
$ nest new <PROJECT_NAME>
專案產生完之後,需額外安裝微服務應用程式相關套件:
$ npm install @nestjs/microservices
透過下方指令啟動應用程式:
$ npm run start:dev
產生 ClientProxy
的方式共有三種,分別是:ClientsModule
、ClientProxyFactory
與 Client Decorator
。
透過 ClientsModule
的 register
靜態方法可以針對不同 Transporter 來建立多個 ClientProxy
,並將其作為 Provider,即可用注入的方式取得。下方是產生使用 TCP Transporter 的 ClientProxy
範例程式碼:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
// ...
@Module({
// ...
imports: [
ClientsModule.register([
{
name: 'SAMPLE_SERVICE',
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: 3333,
},
}
])
]
})
export class AppModule {}
register
帶入的參數為一個物件陣列,裡面的物件有以下三個屬性可以設置:
name
:用來當作該 ClientProxy
的 token
,是一定要帶的參數。transporter
:指定該 ClientProxy
所使用的 Transporter,預設是 TCP Transporter。options
:根據指定的 Transporter 會有不同的設置,設定的格式會跟建立微服務應用程式時,針對不同 Transporter 的相關設置相同。針對 ClientsProxy
的設定內容,有可能會來自設定檔,面對這種情境,可以使用 ClientsModule
提供的非同步處理方案,將 register
改成 registerAsync
靜態方法即可,該靜態方法帶入的參數為一個物件,該物件有一個 clients
屬性,該屬性的型別為物件陣列,可以用來設置多組 ClientProxy
。
假如現在有一個環境變數檔 .env
,內容如下:
SAMPLE_SERVICE_HOST=0.0.0.0
SAMPLE_SERVICE_PORT=3333
此時透過 ConfigModule
來管理這些環境變數,再搭配 registerAsync
就可以讀取環境變數來產生 ClientProxy
。下方為範例程式碼:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
// ...
@Module({
// ...
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ClientsModule.registerAsync({
clients: [
{
name: 'SAMPLE_SERVICE',
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
transport: Transport.TCP,
options: {
host: configService.get('SAMPLE_SERVICE_HOST'),
port: configService.get('SAMPLE_SERVICE_PORT')
}
})
}
],
})
]
})
export class AppModule {}
提醒:關於
ConfigModule
可以參考之前系列文的 Configuration 章節。
ClientsProxy
產生後,可以透過 @Inject
裝飾器將其注入,以下方程式碼為例,在 AppController
注入 token
為 SAMPLE_SERVICE
的 Provider,該 Provider 即先前指定 name
為 SAMPLE_SERVICE
的 ClientProxy
:
import { Controller } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
// ...
}
使用 ClientProxyFactory
可以在不使用 ClientsModule
的情況下產生 ClientProxy
,透過自訂 Provider 的方式即可輕鬆使用。以下方程式碼為例,使用 useFactory
注入 ConfigService
來讀取 ConfigModule
管理的環境變數,並產生 ClientProxy
:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';
// ...
@Module({
// ...
imports: [
ConfigModule.forRoot({ isGlobal: true }),
],
providers: [
{
provide: 'SAMPLE_SERVICE',
useFactory: (configService: ConfigService) => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: {
host: configService.get('SAMPLE_SERVICE_HOST'),
port: configService.get('SAMPLE_SERVICE_PORT'),
},
});
},
inject: [ConfigService],
}
],
})
export class AppModule {}
補充:去看 NestJS 的原始碼會發現,不論是
register
還是registerAsync
,它們產生ClientProxy
都是基於ClientProxyFactory
來產生的。
使用 Client
裝飾器一樣可以在不使用 ClientsModule
的情況下產生 ClientProxy
。下方為範例程式碼,在 AppService
中使用 Client
裝飾器:
import { Inject, Injectable } from '@nestjs/common';
import { Client, ClientProxy, Transport } from '@nestjs/microservices';
@Injectable()
export class AppService {
@Client({
transport: Transport.TCP,
options: {
host: '0.0.0.0',
port: 3333,
},
})
private readonly client: ClientProxy;
// ...
}
但這種產生 ClientProxy
的方式並 不推薦 使用,原因是產生出來的 ClientProxy
不會 被 IoC Container 管理,所以每使用一次就會建立一個實例。
在產生完 ClientProxy
之後,就可以透過它來跟微服務應用程式進行溝通,前面有提到,訊息模式有分 Request-response 與 Event-based 兩種,故ClientProxy
支援這兩種傳訊息的方式。
ClientProxy
提供了 send
方法來傳送訊息,並設計回傳值為 Observable
來等待回應,該方法需帶入兩個參數:
pattern
:需與微服務應用程式在 @MessagePattern
裝飾器中設置的 Pattern 相同。data
:要傳送的資料,具體內容須符合該 API 規格。注意:
send
方法並不是呼叫了就會傳送訊息,要訂閱回傳的Observable
才會送出,當然也可以選擇在 Controller 的 Handler 將Observable
回傳,讓 NestJS 自動訂閱它。
下方為範例程式碼,在 AppController
注入 ClientProxy
,並透過 send
傳送 Pattern 為 { cmd: 'hello' }
的訊息:
import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
@Get()
sayHello() {
return this.client.send({ cmd: 'hello' }, 'HAO');
}
}
微服務應用程式的範例程式碼如下:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
sayHello(data: string) {
return `Hello, ${data}`;
}
}
透過 Postman 使用 GET
方法存取 http://localhost:3000,會看到下方的結果:
Request-response 支援使用串流的方式回傳訊息,如果只是單純將 send
回傳的 Observable
讓 NestJS 自動訂閱,那可能結果會不如預期,原因是 NestJS 會等到該 Observable
進入 complete
狀態再 使用最後一個值做為回傳值,如果說要等到串流完成並將串流過程中的所有回傳值都做處理的話,需要使用 toArray
這個 RxJS 的 operator,它會匯總所有回傳值直到進入 complete
狀態:
import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { toArray } from 'rxjs';
@Controller()
export class AppController {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
@Get()
sayHello() {
return this.client.send({ cmd: 'hello' }, 'HAO').pipe(toArray());
}
}
微服務應用程式的範例程式碼如下:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { delay, from, map } from 'rxjs';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
sayHello(data: string) {
return from([`${data} 1`, `${data} 2`]).pipe(
map((res) => `Hello, ${res}`),
delay(2000)
);
}
}
透過 Postman 使用 GET
方法存取 http://localhost:3000,預期會得到 ["Hello, HAO 1", "Hello, HAO 2"]
:
在微服務的架構下,有時其他服務回應速度過慢會導致依賴它的服務回應速度也跟著變慢,甚至會造成非常長時間的無回應狀態,此時就需要設置 Timeout,RxJS 有提供 timeout
這個 operator,當達到指定時間還沒有收到回應,就會拋出錯誤。下方範例程式碼設置了 5000
毫秒的 Timeout 時間:
import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { timeout } from 'rxjs';
@Controller()
export class AppController {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
@Get()
sayHello() {
return this.client.send({ cmd: 'hello' }, 'HAO').pipe(timeout(5000));
}
}
微服務應用程式的範例程式碼如下:
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { delay, of } from 'rxjs';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
sayHello(data: string) {
return of(`Hello, ${data}`).pipe(
delay(6000)
);
}
}
透過 Postman 使用 GET
方法存取 http://localhost:3000,預期會得到錯誤:
ClientProxy
提供 emit
方法來傳送訊息,該方法需帶入兩個參數:
pattern
:需與微服務應用程式在 @EventPattern
裝飾器中設置的 Pattern 相同。data
:要傳送的資料,具體內容須符合該 API 規格。下方為範例程式碼,在 AppController
注入 ClientProxy
,並透過 emit
傳送 Pattern 為 order.created
的訊息:
import { Controller, Inject, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
@Get('orderCreated')
onOrderCreated() {
this.client.emit('order.created', { name: 'test' });
return {};
}
}
微服務應用程式的範例程式碼如下:
import { Controller } from '@nestjs/common';
import { EventPattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
onOrderCreated(order: { name: string }) {
console.log(order);
}
}
透過 Postman 使用 GET
方法存取 http://localhost:3000/orderCreated,在微服務應用程式的終端機會看到 { name: 'test' }
:
ClientProxy
背後會處理與微服務應用程式的連線,正常情況下,會在第一次送出訊息時建立連線,並且之後每次傳送訊息時都使用該連線,所以可以說 ClientProxy
是 惰性(Lazy) 的。
如果想要在應用程式啟動時,確保一定有連上服務,可以運用 OnApplicationBootstrap
這個 Lifecycle Hook 搭配 ClientProxy
的 connect
方法,如此一來,當沒辦法建立起連線,自身的服務也不會啟動。
提醒:關於 Lifecycle Hooks 可以參考之前系列文的 Lifecycle Hooks 章節。
下方為範例程式碼,在 AppModule
實作 OnApplicationBootstrap
介面,在 onApplicationBootstrap
呼叫 ClientProxy
的 connect
方法並等待其完成:
// ...
@Module({
// ...
})
export class AppModule implements OnApplicationBootstrap {
constructor(
@Inject('SAMPLE_SERVICE')
private readonly client: ClientProxy
) {}
async onApplicationBootstrap() {
await this.client.connect();
}
}
NestJS 實作了與微服務應用程式溝通的客戶端,為了滿足不同的傳輸方式,客戶端採用 Transporter 與 Proxy Pattern 來產生 ClientProxy
,盡可能地保持相同的開發體驗與微服務應用程式溝通。
ClientProxy
產生的方式有三種,分別是:透過 ClientsModule
產生、使用自訂 Provider 的技巧搭配 ClientProxyFactory
來產生、使用 @Client
裝飾器產生。但並不推薦使用 @Client
裝飾器,因為產生出來的 ClientProxy
不會被 IoC Container 管理。
ClientProxy
可以使用 Request-response 與 Event-based 訊息模式來跟微服務應用程式進行溝通,用 send
方法即可傳送訊息並等待回應、用 emit
方法即可單方面傳送訊息給微服務應用程式。
ClientProxy
是惰性的,預設狀況下,會在第一次跟微服務應用程式傳送訊息時建立連線,如果希望在應用程式啟動時確保一定有建立連線,可以使用 OnApplicationBootstrap
這個 Lifecycle Hook,在這裡呼叫 ClientProxy
的 connect
方法並等待它完成。
Hi,很喜歡你推出的 nestjs 系列介紹,有個問題有點好奇,上面文章提到 ClientProxy 產生的方式有三種:透過 ClientsModule、自訂 Provider、@Client decorator
想知道 透過 ClientsModule
、自訂 Provider
這兩種方式在後續有什麼差異嗎?應該如何選用?
謝謝!
Hi 你好,
很高興你喜歡我的文章!
針對 ClientsModule
與自訂 Provider 的差異,有兩個:
ClientsModule
有提供 isGlobal
的選項,可以將 ClientProxy
提升到全域。ClientsModule
產生的 ClientProxy
會在 onApplicationShutdown
這個 Lifecycle Hook 呼叫時執行其 close
方法,這部分可以參考原始碼的設計。上述的特性都只有 ClientsModule
才有,如果沒有特殊需求,我會建議使用 ClientsModule
來產生。